import InvertedLuminanceSource from './../core/InvertedLuminanceSource'
import LuminanceSource from './../core/LuminanceSource'
import Exception from './../core/Exception'

export default class HTMLCanvasElementLuminanceSource extends LuminanceSource  {
    private buffer: Uint8ClampedArray

    private static DEGREE_TO_RADIANS = Math.PI / 180

    private tempCanvasElement: HTMLCanvasElement = null

    public constructor(private canvas: HTMLCanvasElement) {
        super(canvas.width, canvas.height)

        this.buffer = HTMLCanvasElementLuminanceSource.makeBufferFromCanvasImageData(canvas)
    }

    private static makeBufferFromCanvasImageData(canvas: HTMLCanvasElement): Uint8ClampedArray {
        const imageData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height)
        return HTMLCanvasElementLuminanceSource.toGrayscaleBuffer(imageData.data, canvas.width, canvas.height)
    }

    private static toGrayscaleBuffer(imageBuffer: Uint8ClampedArray, width: number, height: number): Uint8ClampedArray {
        const grayscaleBuffer = new Uint8ClampedArray(width * height)
        for(let i = 0, j = 0, length = imageBuffer.length; i < length; i += 4, j++) {
            let gray
            const alpha = imageBuffer[i + 3]
            // The color of fully-transparent pixels is irrelevant. They are often, technically, fully-transparent
            // black (0 alpha, and then 0 RGB). They are often used, of course as the "white" area in a
            // barcode image. Force any such pixel to be white:
            if (alpha === 0) {
                gray = 0xFF
            } else {
                const pixelR = imageBuffer[i]
                const pixelG = imageBuffer[i+1]
                const pixelB = imageBuffer[i+2]
                // .299R + 0.587G + 0.114B (YUV/YIQ for PAL and NTSC),
                // (306*R) >> 10 is approximately equal to R*0.299, and so on.
                // 0x200 >> 10 is 0.5, it implements rounding.
                gray = (306 * pixelR +
                    601 * pixelG +
                    117 * pixelB +
                    0x200) >> 10
            }
            grayscaleBuffer[j] = gray
        }
        return grayscaleBuffer
    }

    public getRow(y: number /*int*/, row: Uint8ClampedArray): Uint8ClampedArray {
        if (y < 0 || y >= this.getHeight()) {
            throw new Exception(Exception.IllegalArgumentException, "Requested row is outside the image: " + y)
        }
        const width: number /*int*/ = this.getWidth()
        const start = y * width
        if (row === null ) {
            row = this.buffer.slice(start, start + width)
        } else {
            if (row.length < width) {
                row = new Uint8ClampedArray(width)
            }
            // The underlying raster of image consists of bytes with the luminance values
            // TODO: can avoid set/slice?
            row.set(this.buffer.slice(start, start + width))
        }

        return row
    }

    public getMatrix(): Uint8ClampedArray {
        return this.buffer
    }

    public isCropSupported(): boolean {
        return true
    }

    public crop(left: number /*int*/, top: number /*int*/, width: number /*int*/, height: number /*int*/): LuminanceSource {
        this.crop(left, top, width, height)
        return this
    }

    /**
     * This is always true, since the image is a gray-scale image.
     *
     * @return true
     */
    public isRotateSupported(): boolean {
        return true
    }

    public rotateCounterClockwise(): LuminanceSource {
        this.rotate(-90)
        return this
    }

    public rotateCounterClockwise45(): LuminanceSource {
        this.rotate(-45)
        return this
    }

    private getTempCanvasElement() {
        if (null === this.tempCanvasElement) {
            const tempCanvasElement = this.canvas.ownerDocument.createElement('canvas')
            tempCanvasElement.style.width = `${this.canvas.width}px`
            tempCanvasElement.style.height = `${this.canvas.height}px`
        }

        return this.tempCanvasElement
    }

    private rotate(angle: number) {
        const tempCanvasElement = this.getTempCanvasElement()
        const tempContext = tempCanvasElement.getContext('2d')
        tempContext.rotate(angle * HTMLCanvasElementLuminanceSource.DEGREE_TO_RADIANS)
        tempContext.drawImage(this.canvas, 0, 0)
        this.buffer = HTMLCanvasElementLuminanceSource.makeBufferFromCanvasImageData(tempCanvasElement)
        return this
    }

    public invert(): LuminanceSource {
        return new InvertedLuminanceSource(this)
    }
}
